Задача
Оцените корректность проведения теста и проанализировать его результаты. Чтобы оценить корректность проведения теста:
удостоверьтесь, что нет пересечений с конкурирующим тестом и нет пользователей, участвующих в двух группах теста одновременно;
проверьте равномерность распределения пользователей по тестовым группам и правильность их формирования.
Техническое задание
Название теста: recommender_system_test ;
группы: А — контрольная, B — новая платёжная воронка;
дата запуска: 2020-12-07;
дата остановки набора новых пользователей: 2020-12-21;
дата остановки: 2021-01-04;
аудитория: в тест должно быть отобрано 15% новых пользователей из региона EU;
назначение теста: тестирование изменений, связанных с внедрением улучшенной рекомендательной системы;
ожидаемое количество участников теста: 6000.
ожидаемый эффект: за 14 дней с момента регистрации пользователи покажут улучшение каждой метрики не менее, чем на 10%:
конверсии в просмотр карточек товаров — событие product_page,
просмотры корзины — product card,
покупки — purchase.
#импорт библиотек
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
import pandas as pd
import scipy.stats as stats
import numpy as np
import seaborn as sn
from matplotlib import pyplot as plt
import datetime as dt
import math
import plotly.express as px
from datetime import timedelta, datetime
from plotly import graph_objects as go
#загружаем данные
events = pd.read_csv('https://code.s3.yandex.net/datasets/final_ab_events.csv')
new_users = pd.read_csv('https://code.s3.yandex.net/datasets/final_ab_new_users.csv')
marketing_events = pd.read_csv('https://code.s3.yandex.net/datasets/ab_project_marketing_events.csv')
participants = pd.read_csv('https://code.s3.yandex.net/datasets/final_ab_participants.csv')
#функция для вывода основных методов при исследовании данных
def data_research(df):
print('Вывод первых 5 строк датафрейма')
print('*'*50)
display(df.head())
print('*'*50)
print('Описание данных методом describe()')
print('*'*50)
display(df.describe().T)
print('*'*50)
print('Описание данных методом info()')
print('*'*50)
df.info()
print('*'*50)
print('Количество дубликатов')
print('*'*50)
print(df.duplicated().sum())
print('*'*50)
print('Количество пропусков')
print('*'*50)
print(df.isna().sum())
#изучаем events
data_research(events)
Вывод первых 5 строк датафрейма **************************************************
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
************************************************** Описание данных методом describe() **************************************************
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| details | 62740.0 | 23.877631 | 72.180465 | 4.99 | 4.99 | 4.99 | 9.99 | 499.99 |
************************************************** Описание данных методом info() ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null object 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: float64(1), object(3) memory usage: 13.4+ MB ************************************************** Количество дубликатов ************************************************** 0 ************************************************** Количество пропусков ************************************************** user_id 0 event_dt 0 event_name 0 details 377577 dtype: int64
Описание данных
final_ab_events.csv — действия новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.
Структура файла:
user_id — идентификатор пользователя;
event_dt — дата и время покупки;
event_name — тип события;
details — дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах.
Колонка details иммет пропуски, вместо 440317 строк 62740. Но согласно техническому заданию, в колонку заполняются дополнительные сведения, и для каких-то типов событий эти сведения могут отсутсвовать. Тип столбца event_dt необходимо привести к специальному типу datetime.
#меняем тип данных даты
events['event_dt'] = pd.to_datetime(events['event_dt'], format='%Y-%m-%d %H:%M:%S')
print(f'Количество уникальных пользователей в events - {events["user_id"].nunique()} ')
Количество уникальных пользователей в events - 58703
print(f'уникальные события в events - {events["event_name"].unique()} ')
уникальные события в events - ['purchase' 'product_cart' 'product_page' 'login']
Описание событий следующее:
конверсии в просмотр карточек товаров — событие product_page,
просмотры корзины — product card,
покупки — purchase,
регистрация - login.
#даты покупок
events['event_dt'].unique()
array(['2020-12-07T20:22:03.000000000', '2020-12-07T09:22:53.000000000',
'2020-12-07T12:59:29.000000000', ...,
'2020-12-30T12:21:24.000000000', '2020-12-30T10:54:15.000000000',
'2020-12-30T10:59:09.000000000'], dtype='datetime64[ns]')
#проверим для каких колонок заполнено details
for i in events['event_name'].unique():
print(f'Для события - {i}')
print(events.query('event_name==@i')['details'].unique())
Для события - purchase [ 99.99 9.99 4.99 499.99] Для события - product_cart [nan] Для события - product_page [nan] Для события - login [nan]
Колонка details заполнена только для типа события - purchase (покупка).
Покупки совершаются с 7 декабря 2020 по 30 декабря 2020.
#изучаем marketing_events
data_research(marketing_events)
Вывод первых 5 строк датафрейма **************************************************
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
************************************************** Описание данных методом describe() **************************************************
| count | unique | top | freq | |
|---|---|---|---|---|
| name | 14 | 14 | St. Patric's Day Promo | 1 |
| regions | 14 | 6 | APAC | 4 |
| start_dt | 14 | 14 | 2020-11-11 | 1 |
| finish_dt | 14 | 14 | 2020-12-01 | 1 |
************************************************** Описание данных методом info() ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes ************************************************** Количество дубликатов ************************************************** 0 ************************************************** Количество пропусков ************************************************** name 0 regions 0 start_dt 0 finish_dt 0 dtype: int64
Описание данных
ab_project_marketing_events.csv — календарь маркетинговых событий на 2020 год.
Структура файла:
name — название маркетингового события;
regions — регионы, в которых будет проводиться рекламная кампания;
start_dt — дата начала кампании;
finish_dt — дата завершения кампании.
#выведем всю таблицу для ознакомления
marketing_events
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
| 5 | Black Friday Ads Campaign | EU, CIS, APAC, N.America | 2020-11-26 | 2020-12-01 |
| 6 | Chinese New Year Promo | APAC | 2020-01-25 | 2020-02-07 |
| 7 | Labor day (May 1st) Ads Campaign | EU, CIS, APAC | 2020-05-01 | 2020-05-03 |
| 8 | International Women's Day Promo | EU, CIS, APAC | 2020-03-08 | 2020-03-10 |
| 9 | Victory Day CIS (May 9th) Event | CIS | 2020-05-09 | 2020-05-11 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
| 11 | Dragon Boat Festival Giveaway | APAC | 2020-06-25 | 2020-07-01 |
| 12 | Single's Day Gift Promo | APAC | 2020-11-11 | 2020-11-12 |
| 13 | Chinese Moon Festival | APAC | 2020-10-01 | 2020-10-07 |
Дубликаты и пропуски отсуствуют в датафрейме. Столбцы start_dt и finish_dt необходимо привести к специальному типу datetime.
#меняем тип данных даты
marketing_events['start_dt'] = pd.to_datetime(marketing_events['start_dt'], format='%Y-%m-%d')
marketing_events['finish_dt'] = pd.to_datetime(marketing_events['finish_dt'], format='%Y-%m-%d')
Маркетинговые события проводятся в Европе, Северной Америке, СНГ и странах Азии.
Всего в датасете представлено 14 событий:
- промо-акции
- день одиночки;
- Международный день женщин;
- Китайский новый год;
- 4 июля;
- пасха;
- день Святого Патрика;
- Рождество и Новый год.
- рекламные акции
- черная пятница;
- день труда (1 мая);
- 9 мая;
- розыгрыши подарков
- Новый год СНГ;
- день святого валентина.
- фестивали
- фестиваль середины осени;
- фестиваль драконьих лодок.
Причем промоакция "Christmas&New Year Promo" (с 25 декабря по 3 января) пересеклась с датами проведения теста. Необходимо изучить визуализацию распределения событий и понять были ли в эти даты резкие скачки или резкое падения количества событий.
#изучаем new_users
data_research(new_users)
Вывод первых 5 строк датафрейма **************************************************
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
************************************************** Описание данных методом describe() **************************************************
| count | unique | top | freq | |
|---|---|---|---|---|
| user_id | 61733 | 61733 | 028C841A61F126F5 | 1 |
| first_date | 61733 | 17 | 2020-12-21 | 6290 |
| region | 61733 | 4 | EU | 46270 |
| device | 61733 | 4 | Android | 27520 |
************************************************** Описание данных методом info() ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object dtypes: object(4) memory usage: 1.9+ MB ************************************************** Количество дубликатов ************************************************** 0 ************************************************** Количество пропусков ************************************************** user_id 0 first_date 0 region 0 device 0 dtype: int64
Описание данных
final_ab_new_users.csv - все пользователи, зарегистрировавшиеся в интернет-магазине в период с 7 по 21 декабря 2020 года.
Структура файла:
user_id — идентификатор пользователя;
first_date — дата регистрации;
region — регион пользователя;
device — устройство, с которого происходила регистрация.
Пропуски отсутсвуют, дубликаты тоже. Тип столбца first_date необходимо привести к специальному типу datetime.
#меняем тип данных даты
new_users['first_date'] = pd.to_datetime(new_users['first_date'], format='%Y-%m-%d')
print(f'Количество уникальных пользователей в new_users - {new_users["user_id"].nunique()} ')
Количество уникальных пользователей в new_users - 61733
#регионы пользователей
new_users["region"].unique()
array(['EU', 'N.America', 'APAC', 'CIS'], dtype=object)
#устройства пользователей
new_users["device"].unique()
array(['PC', 'Android', 'iPhone', 'Mac'], dtype=object)
#даты регистраций пользователей
new_users["first_date"].sort_values().unique()
array(['2020-12-07T00:00:00.000000000', '2020-12-08T00:00:00.000000000',
'2020-12-09T00:00:00.000000000', '2020-12-10T00:00:00.000000000',
'2020-12-11T00:00:00.000000000', '2020-12-12T00:00:00.000000000',
'2020-12-13T00:00:00.000000000', '2020-12-14T00:00:00.000000000',
'2020-12-15T00:00:00.000000000', '2020-12-16T00:00:00.000000000',
'2020-12-17T00:00:00.000000000', '2020-12-18T00:00:00.000000000',
'2020-12-19T00:00:00.000000000', '2020-12-20T00:00:00.000000000',
'2020-12-21T00:00:00.000000000', '2020-12-22T00:00:00.000000000',
'2020-12-23T00:00:00.000000000'], dtype='datetime64[ns]')
Всего датасет содержит 61733 уникальных пользователей, привлеченных с 7 декабря 2020 по 23 декабря 2020. Пользователи используют следующие типов устройств: PC, Android, iPhone, Mac. Регионы привлеченных пользователей: EU, N.America, APAC, CIS.
Согласно техническому заданию дата остановки набора новых пользователей: 2020-12-21. Поэтому данный датасет придется отфильтровать на этапе проверки соответсвия данных техническому заданию.
#изучаем participants
data_research(participants)
Вывод первых 5 строк датафрейма **************************************************
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
************************************************** Описание данных методом describe() **************************************************
| count | unique | top | freq | |
|---|---|---|---|---|
| user_id | 18268 | 16666 | 5424E9D321EC3567 | 2 |
| group | 18268 | 2 | A | 9655 |
| ab_test | 18268 | 2 | interface_eu_test | 11567 |
************************************************** Описание данных методом info() ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB ************************************************** Количество дубликатов ************************************************** 0 ************************************************** Количество пропусков ************************************************** user_id 0 group 0 ab_test 0 dtype: int64
#уникальные пользователи
participants['user_id'].nunique()
16666
# проводимые тесты
participants['ab_test'].unique()
array(['recommender_system_test', 'interface_eu_test'], dtype=object)
# группы пользователей
participants['group'].unique()
array(['A', 'B'], dtype=object)
#изучим количество пользователей в нашем тесте
participants.query('ab_test=="recommender_system_test"').groupby('group').agg(users_by_group=('group','count')).reset_index()
| group | users_by_group | |
|---|---|---|
| 0 | A | 3824 |
| 1 | B | 2877 |
Описание данных
/datasets/final_ab_participants.csv — таблица участников тестов.
Структура данных:
user_id — идентификатор пользователя;
ab_test — название теста;
group — группа пользователя
Пропуски и дубликаты отсутствуют. Типы данных соответсвуют описанию. Всего датафрейм содержит 18268 строк и 16666 уникальных пользователей. Имеются данные о двух A/B-тестах: recommender_system_test и interface_eu_test. Согласно техническому заданию нас интересует только recommender_system_test. Для данного теста пользователи поделены следующим образом: группа A - контрольная, имеет 3824 человека, группа B - новая платёжная воронка, содержит 2877 человек.
Вероятно, какие-то пользователи попали в два теста или в две группы.
Таким образом, мы изучили 4 датафрейма. Дубликаты отсуствуются во всех датасетах. Пропуски присуствуют в events в столбце details, но они обусловлены особенностью заполнения столбца. Кроме того, столбец details заполнен только для типа события - purchases (покупка).
Все столбцы с датой приведены к сепциальному типу datetime.
На этапе ознакомления с данными таблицы new_users было отмечено, что дата остановки набора новых пользователей 2020-12-23 не соответсвует техническому заданию - 2020-12-21.
Также количество уникальных пользователей в таблице participants не соответсвует количеству строк в таблице, необходимо проверить нет ли пересечений групп пользователей, или возможно это персечение тестов (и тогда нет необходимости удалять данные).
#сохраним сырые данные
participants_row = participants
#начнем с первого требования, нас интересует только тест - recommender_system_test
participants = participants.query('ab_test=="recommender_system_test"')
print(f'В датафрейме было {round((len(participants)/len(participants_row)*100),2)}% об интересующем нас тесте - recommender_system_test')
В датафрейме было 36.68% об интересующем нас тесте - recommender_system_test
#группы пользователей
participants_group = participants.groupby('group').agg(users_by_group=('group','count')).reset_index()
participants_group
| group | users_by_group | |
|---|---|---|
| 0 | A | 3824 |
| 1 | B | 2877 |
Таким образом, мы оставили в таблице participants только данные о тесте - recommender_system_test, он составляет 36% исходных данных.
В разделе "Аудитория теста" проверим нет ли пересечений групп теста "А" и "В", и нет ли персечений с конкурирующим тестом.
# проверим дату действий пользователя на соотвествие началу (2020-12-07) и концу тесту (2021-01-04)
print(f'Дата первого действия пользователя - {events["event_dt"].min()}')
print(f'Дата последнего действия пользователя - {events["event_dt"].max()}')
Дата первого действия пользователя - 2020-12-07 00:00:33 Дата последнего действия пользователя - 2020-12-30 23:36:33
Таким образом, дейстия пользователей не вышли за временные рамки проведения теста.
Согласно техническому заданию дата остановки набора новых пользователей - 21 декабря 2020 года. При обзоре данных мы вяснили, что в таблице new_users, которая содержит сведения о регистрации новых пользователей, последняя дата - 23 декабря 2020. Приведем данные таблицы new_users в соответсвие ТЗ.
#сохраним сырые данные
new_users_row = new_users
new_users = new_users.query('first_date<="2020-12-21"')
print(f'После фильтрации осталось {round((len(new_users)/len(new_users_row)*100),2)}% ')
После фильтрации осталось 91.47%
#определим процент новых пользователей из Европы
eu_new_users = new_users[new_users['region'] == 'EU']['user_id']
eu_users_from_test = participants.query('user_id in @eu_new_users')['user_id']
print(f'Доля от всех зарегистрированных из этого региона в период отбора пользователей:{round(eu_users_from_test.nunique() / eu_new_users.nunique()*100,2)}%')
print(f'Всего пользователей из Европы в тесте - {eu_users_from_test.nunique()}')
print(f'Всего новых пользователей из Европы - {eu_new_users.nunique()}')
Доля от всех зарегистрированных из этого региона в период отбора пользователей:15.0% Всего пользователей из Европы в тесте - 6351 Всего новых пользователей из Европы - 42340
#распределение новых пользователей по регионами
new_users_by_regions = new_users.groupby('region').agg(count=('user_id', 'count'))
#проверим рспределение людей в нашем тесте по регионами
#объединим таблицы
check_users = participants.merge(new_users, on='user_id', how='left').reset_index(drop=True)
check_users.head()
| user_id | group | ab_test | first_date | region | device | |
|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC |
| 1 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone |
| 2 | DABC14FDDFADD29E | A | recommender_system_test | 2020-12-08 | EU | Mac |
| 3 | 04988C5DF189632E | A | recommender_system_test | 2020-12-14 | EU | iPhone |
| 4 | 482F14783456D21B | B | recommender_system_test | 2020-12-14 | EU | PC |
#сгруппируем пользователей по регионам и группам и посмотрим, как они распределены между группами
check_users = check_users.groupby(['group','region']).agg(user_count=('user_id','count')).reset_index()
check_users = check_users.merge(participants_group, how='left', on='group')
check_users
| group | region | user_count | users_by_group | |
|---|---|---|---|---|
| 0 | A | APAC | 37 | 3824 |
| 1 | A | CIS | 25 | 3824 |
| 2 | A | EU | 3634 | 3824 |
| 3 | A | N.America | 128 | 3824 |
| 4 | B | APAC | 35 | 2877 |
| 5 | B | CIS | 30 | 2877 |
| 6 | B | EU | 2717 | 2877 |
| 7 | B | N.America | 95 | 2877 |
#посчитаем долю для каждого региона
check_users['percent'] = round(check_users['user_count']/check_users['users_by_group']*100,2)
check_users
| group | region | user_count | users_by_group | percent | |
|---|---|---|---|---|---|
| 0 | A | APAC | 37 | 3824 | 0.97 |
| 1 | A | CIS | 25 | 3824 | 0.65 |
| 2 | A | EU | 3634 | 3824 | 95.03 |
| 3 | A | N.America | 128 | 3824 | 3.35 |
| 4 | B | APAC | 35 | 2877 | 1.22 |
| 5 | B | CIS | 30 | 2877 | 1.04 |
| 6 | B | EU | 2717 | 2877 | 94.44 |
| 7 | B | N.America | 95 | 2877 | 3.30 |
#строим график
fig = px.bar(check_users, y='user_count', x='region', color='group', text='percent', title='Распределение пользователей регионов по группам теста')
fig.update_layout(yaxis_title = 'Количество пользователей', xaxis_title = 'Регион')
fig.show()
#строим график без EU
fig = px.bar(check_users.query('region != "EU"'), y='user_count', x='region', color='group', text='percent', title='Распределение пользователей регионов по группам теста')
fig.update_layout(yaxis_title = 'Количество пользователей', xaxis_title = 'Регион')
fig.show()
Пользователи распределены не совсем равномерно по группам теста, в то время как для СНГ и стра Азии - доля пользователей около 1%, для Северной Америки - 3,3% Возможно пользователи пользовались VPN и поэтому попали в другие регионы, а возможно при проведении теста случился сбой и новая версия показывалась и пользователям из других стран.
Примем решение оставить только пользователей из Европы, исключив СНГ, страны Азии и Северную Америку.
#отфильтруем новых пользователей по региону
new_users = new_users[new_users['region'] == 'EU']
new_users.shape[0]
42340
#отфильтруем пользователей теста по региону
participants = participants.query('user_id in @eu_new_users')
participants.shape[0]
6351
Аудитория ( 15% новых пользователей из региона EU) соответсвует ТЗ. Ожидаемое количество участников теста (6000) и лайфтайм 14 дней проверим в самом конце обработки данных.
print(f'Дата первого действия пользователя - {events["event_dt"].min()}')
print(f'Дата последнего действия пользователя - {events["event_dt"].max()}')
print(f'Даты регистрации пользователей с {new_users["first_date"].min()} по {new_users["first_date"].max()} ')
Дата первого действия пользователя - 2020-12-07 00:00:33 Дата последнего действия пользователя - 2020-12-30 23:36:33 Даты регистрации пользователей с 2020-12-07 00:00:00 по 2020-12-21 00:00:00
#функция для проверки персечение дат
def intersect(df,t1end=dt.date(2021, 1, 4) ,t1start=dt.date(2020, 12, 7)):
listik = []
for i in df['name'].unique():
print(f'для события - {i}')
t2end = df.query('name==@i')['finish_dt'].max()
t2start = df.query('name==@i')['start_dt'].max()
if (t1start <= t2start <= t2end <= t1end) and ("EU" in ''.join(df.query('name==@i')['regions'].to_list())):
print(f'Пересечение{t2start,t2end}')
print('')
elif (t1start <= t2start <= t1end) and ("EU" in ''.join(df.query('name==@i')['regions'].to_list())):
print(f'Пересечение{t2start,t1end}')
print('')
elif (t1start <= t2end <= t1end) and ("EU" in ''.join(df.query('name==@i')['regions'].to_list())):
print(f'Пересечение{t1start,t2end}')
print('')
elif (t2start <= t1start <= t1end <= t2end) and ("EU" in ''.join(df.query('name==@i')['regions'].to_list())):
print(f'Пересечение {t1start,t1end}')
print('')
else:
print('Нет пересечений')
print('')
return
intersect(marketing_events)
для события - Christmas&New Year Promo
Пересечение(Timestamp('2020-12-25 00:00:00'), Timestamp('2021-01-03 00:00:00'))
для события - St. Valentine's Day Giveaway
Нет пересечений
для события - St. Patric's Day Promo
Нет пересечений
для события - Easter Promo
Нет пересечений
для события - 4th of July Promo
Нет пересечений
для события - Black Friday Ads Campaign
Нет пересечений
для события - Chinese New Year Promo
Нет пересечений
для события - Labor day (May 1st) Ads Campaign
Нет пересечений
для события - International Women's Day Promo
Нет пересечений
для события - Victory Day CIS (May 9th) Event
Нет пересечений
для события - CIS New Year Gift Lottery
Нет пересечений
для события - Dragon Boat Festival Giveaway
Нет пересечений
для события - Single's Day Gift Promo
Нет пересечений
для события - Chinese Moon Festival
Нет пересечений
На период проведения теста попало маркетинговое событие "Christmas&New Year Promo". Проверим распределение событий по дням.
#объединим таблицы для графика
ab_test_temporary= participants.merge(events, on='user_id', how='left').reset_index(drop=True)
ab_test_temporary.head()
| user_id | group | ab_test | event_dt | event_name | details | |
|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | purchase | 99.99 |
| 1 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:56 | purchase | 4.99 |
| 2 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:29 | product_cart | NaN |
| 3 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:57 | product_cart | NaN |
| 4 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | product_page | NaN |
#гистограмма по дате и времени
plt.figure(figsize=(15,5))
sn.histplot(data = ab_test_temporary, x='event_dt',bins=24*24, hue='group', element="step")
plt.title('Гистограмма распределения событий ')
plt.xlabel('Дата')
plt.ylabel('Количество событий')
plt.grid()
plt.show()
По гистограмме видно, что в период проведения маркетинговых событий не наблюдается резких скачков или спадов. Заметно, что отсутствуют данные с 30 декабря по 4 января (по ТЗ тест еще длилися в это время, возможно это обусловлено технической ошибкой при выгрузке данных.
#сохраним сырые данные
events_row = events
#events = events.query('event_dt<"2020-12-25"')
#print(f'После фильтрации потеряли {round((100 - len(events)/len(events_row)*100),2)}% ')
events.shape[0]
440317
#проверим наличие пользователей в двух тестах
in_both_ab_groups = participants_row.groupby('user_id').agg({'ab_test':'nunique'}).query('ab_test > 1').reset_index()
len(in_both_ab_groups)
1602
#сохраним пользователей, попавших в обе группы
participants_both = participants_row[participants_row['user_id'].isin(in_both_ab_groups['user_id'])]
participants_both.sample(10)
| user_id | group | ab_test | |
|---|---|---|---|
| 16380 | F19288FFDF6014FE | A | interface_eu_test |
| 2116 | B9AF175E0AAECD8C | A | recommender_system_test |
| 7885 | 1D055BBA227A1E7F | A | interface_eu_test |
| 1055 | 71D1FF7218FB3F1E | A | recommender_system_test |
| 1656 | F1B46AFD720C74D8 | B | recommender_system_test |
| 4727 | 36EDA624DB7B7F90 | A | recommender_system_test |
| 9410 | CCAF138D12385DF2 | A | interface_eu_test |
| 4770 | 7E19C338FE4D995A | B | recommender_system_test |
| 14814 | 7E0D60AF41DECEC6 | A | interface_eu_test |
| 3329 | 47219A3FA0F71DE0 | B | recommender_system_test |
При решении того, каких пользователей конкурирующего теста мы отфильтруем, важно ли представлять, чем отличаются группы А и Б конкурирующего теста? Важно ли, равномерно ли они распределены между группами нашего теста?
Если пользователи попали в группу А конкурирующего теста, то можно их оставлять, так как это контрольная группа и на нее не оказывается никакого влияния.
А вот с пользователями группы В ситуация обратная, надо рассмотреть, сколько пользователей попали из конкурирующего теста в группу В. Посмотреть насколько равномерно они распределены в нашем тесте, если относительно равномерно, то их при можно оставить.
#пользователя группы В конкурирующего теста
participants_B = participants_both.query('ab_test == "interface_eu_test" & group =="B"')
len(participants_B)
783
#сохраним пользователей в лист
participants_B_list = participants_B['user_id']
#рассмотрим, как данные пользователи распределились по нашим группам
participants_recomm = participants.query('user_id in @participants_B_list and ab_test=="recommender_system_test"')\
.groupby(['ab_test', 'group']).agg({'user_id': 'nunique'})\
.reset_index()
participants_recomm
| ab_test | group | user_id | |
|---|---|---|---|
| 0 | recommender_system_test | A | 439 |
| 1 | recommender_system_test | B | 344 |
Проверим равномерность распределение пользователей в нашем тесте при помощи z-теста.
Сформулируем гипотезы.
Нулевая: доли пользователей в группах А и В равны.
Альтернативная: доли пользователей в группах различаются.
#статистический уровень значимости
alpha = 0.05
event_1 = participants_recomm.query('group=="A"')['user_id'].max()
total_1 = participants.groupby('group').agg(users_by_group=('group','count')).reset_index()['users_by_group'][0]
event_2 = participants_recomm.query('group=="B"')['user_id'].max()
total_2 = participants.groupby('group').agg(users_by_group=('group','count')).reset_index()['users_by_group'][1]
#доля успехов в 1 группе
share_1 = event_1 / total_1
#доля успехов в 2 группе
share_2 = event_2 / total_2
#доля успехов в комбинированном датасете
share_combined = (event_1 + event_2) / (total_1 + total_2)
#разница в долях между группами
diff = share_1 - share_2
#считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = diff / (share_combined * (1 - share_combined) * (1/total_1 + 1/total_2)) ** 0.5
#задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
dist = stats.norm(0, 1)
#считаем по статистичке p-value
p_value = 2 * (1 - dist.cdf(abs(z_value)))
print('p-значение: ', p_value)
#сравниваем p_value с уровнем значимости и делаем вывод по тесту
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
p-значение: 0.4861791801372526 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
#проверим наличие пользователей в двух группах
in_both_groups = ab_test_temporary.groupby('user_id').agg({'group':'nunique'}).query('group > 1')
len(in_both_groups)
0
#проверим равномерность распределение пользователей в нашем тесте
#подготовим таблицу для теста
our_test = participants.groupby('group').agg(users_by_group=('user_id','nunique')).reset_index()
our_test['all'] = our_test['users_by_group'].sum()
our_test
| group | users_by_group | all | |
|---|---|---|---|
| 0 | A | 3634 | 6351 |
| 1 | B | 2717 | 6351 |
Проверим равномерность распределение пользователей в нашем тесте при помощи z-теста.
Сформулируем гипотезы.
Нулевая: доли пользователей в группах А и В равны.
Альтернативная: доли пользователей в группах различаются.
#статистический уровень значимости
alpha = 0.05
event_1 = our_test.query('group=="A"')['users_by_group'].max()
total_1 = our_test.query('group=="A"')['all'].max()
event_2 = our_test.query('group=="B"')['users_by_group'].max()
total_2 = our_test.query('group=="A"')['all'].max()
#доля успехов в 1 группе
share_1 = event_1 / total_1
#доля успехов в 2 группе
share_2 = event_2 / total_2
#доля успехов в комбинированном датасете
share_combined = (event_1 + event_2) / (total_1 + total_2)
#разница в долях между группами
diff = share_1 - share_2
#считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = diff / (share_combined * (1 - share_combined) * (1/total_1 + 1/total_2)) ** 0.5
#задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
dist = stats.norm(0, 1)
#считаем по статистичке p-value
p_value = 2 * (1 - dist.cdf(abs(z_value)))
print('p-значение: ', p_value)
#сравниваем p_value с уровнем значимости и делаем вывод по тесту
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
p-значение: 0.0 Отвергаем нулевую гипотезу: между долями есть значимая разница
Всего в группах 6351 пользователь. Пользователей в двух группах "А" и "В" - нет. Пользователи между группами "А" и "Б" распределены неравномерно.
#объединим таблицы для теста
ab_test = (participants.merge(events, on='user_id', how='left')
.merge(new_users, on='user_id', how='left')
.reset_index(drop=True)
)
ab_test.head(10)
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | purchase | 99.99 | 2020-12-07 | EU | PC |
| 1 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:56 | purchase | 4.99 | 2020-12-07 | EU | PC |
| 2 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:29 | product_cart | NaN | 2020-12-07 | EU | PC |
| 3 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:57 | product_cart | NaN | 2020-12-07 | EU | PC |
| 4 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | product_page | NaN | 2020-12-07 | EU | PC |
| 5 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:57 | product_page | NaN | 2020-12-07 | EU | PC |
| 6 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | login | NaN | 2020-12-07 | EU | PC |
| 7 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:56 | login | NaN | 2020-12-07 | EU | PC |
| 8 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 15:46:06 | product_page | NaN | 2020-12-20 | EU | iPhone |
| 9 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-21 00:40:59 | product_page | NaN | 2020-12-20 | EU | iPhone |
#сохраним сырые данные
ab_test_row = ab_test
#добавим столбец с разницей между регистрацией и событием
ab_test['difference'] = ab_test['event_dt'] - ab_test['first_date']
#проверим пользователей, которые не совершали событий
ab_test[ab_test['event_dt'].isna()]
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | difference | |
|---|---|---|---|---|---|---|---|---|---|---|
| 32 | 482F14783456D21B | B | recommender_system_test | NaT | NaN | NaN | 2020-12-14 | EU | PC | NaT |
| 59 | 057AB296296C7FC0 | B | recommender_system_test | NaT | NaN | NaN | 2020-12-17 | EU | iPhone | NaT |
| 66 | E9FA12FAE3F5769C | B | recommender_system_test | NaT | NaN | NaN | 2020-12-14 | EU | Android | NaT |
| 67 | FDD0A1016549D707 | A | recommender_system_test | NaT | NaN | NaN | 2020-12-13 | EU | PC | NaT |
| 68 | 547E99A7BDB0FCE9 | A | recommender_system_test | NaT | NaN | NaN | 2020-12-12 | EU | iPhone | NaT |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 26219 | C5E1BD2400840B30 | B | recommender_system_test | NaT | NaN | NaN | 2020-12-17 | EU | iPhone | NaT |
| 26220 | EA29547AB3C0CB9C | B | recommender_system_test | NaT | NaN | NaN | 2020-12-14 | EU | iPhone | NaT |
| 26242 | 9A44E27079666291 | B | recommender_system_test | NaT | NaN | NaN | 2020-12-08 | EU | Android | NaT |
| 26243 | 9C2D0067A991213E | B | recommender_system_test | NaT | NaN | NaN | 2020-12-07 | EU | PC | NaT |
| 26258 | A23B0A7FFF375BFF | B | recommender_system_test | NaT | NaN | NaN | 2020-12-12 | EU | Android | NaT |
2870 rows × 10 columns
print(f'Исходное количество строк в общей таблице - {ab_test.shape[0]}')
Исходное количество строк в общей таблице - 26290
Так как нас интересует улучшение метрики только в течении 14 дней от регистрации, мы должны отбросить события, которые произошли после 14 дней от того момента, как был зарегистрирован пользователь, который их совершил. Нам необходимо, чтобы лайфтайм событий, которые будут влиять на наш тест, укладывался в установленный горизонт событий.
#отфильтруем события, которые пользователи совершили после 14 дней от регистрации
ab_test = ab_test.query('difference <= "14 days"')
print(f'Количество строк в общей таблице - {ab_test.shape[0]}')
Количество строк в общей таблице - 22620
#проверим в течение какого времени пользователи совершают большую часть событий
days = ["1 day","2 day","3 day", "4 day", "5 day", "6 days","7 days", "8 days", "9 days", "10 days", "11 days", "12 days", "13 days","14 days"]
for j in ab_test['event_name'].unique():
#print('Для события', j)
#print('')
count_event = []
for i in days:
ab_tem = ab_test.query('event_name==@j')
#print(f'Количество событий {ab_tem.query("difference < @i")["difference"].count()}')
count_event.append(ab_tem.query("difference < @i")["user_id"].count())
plt.figure(figsize=(10,5))
plt.title(f'Динамика по лайфтайму для события - {j}')
plt.xlabel('Горизонт')
plt.ylabel('Количество событий')
sn.lineplot(x=days, y=count_event, color = 'blue')
plt.xticks(rotation=15)
plt.grid()
Ожидаемое количество участников теста проверим после построения продуктовой воронки, значение конверсии в покупку для группы А и будет исходным значением конверсии.
Таким образом, мы отфильтровали данные по:
дате окончания набора новых пользователей ( 21 декабря 2020);
дате начала и окончания теста (в целом не выходим за границы ТЗ, но почему-то дата последнего события - 30 декабря, а тест продолжался до 4 января, часть данных была потеряна?);
региону проведения теста - Европа.
Подтвердили, что в тест попали 15% новых пользователей из Европы.
Проверили пересечение с маркетинговыми события, и выяснили, что попали на проведение Рождественского&Новогоднего промо. По гистограмме распределения событий по дням всплеска/спада не было выявлено.
Не стали исключать пользователей, попавших в конкурирующий тест, так как они распределены равномерно между группами нашего теста.
Исключили события, у которых горизонт превышал 14 дней. Но оставили пользователей не прожившихся 14 дней, так как в среднем большая часть событий совершается в течение 5 дней. Исключив пользователей, мы бы заметно снизили мощность теста.
Также исключили пользователей без событий, в результате остался 3481 пользователь (в группе А - 2604, в группе B - 877).
#удалим лишние столбцы
ab_test.drop(['difference', 'region'], axis= 1 , inplace= True)
/Users/elizavetapuhova/opt/anaconda3/envs/da_practicum_env/lib/python3.9/site-packages/pandas/core/frame.py:4308: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
#количество участников в тесте
ab_test['user_id'].nunique()
3481
#количество пользователей в группах
ab_test.groupby('group')['user_id'].nunique()
group A 2604 B 877 Name: user_id, dtype: int64
#построим продуктовую воронку для групп A и В
funnel_event_A = (ab_test
.query('group == "A"')
.groupby('event_name')
.agg({'user_id':'nunique'})
.reset_index()
.sort_values(by='user_id', ascending=False)
.reset_index(drop=True))
funnel_event_A.columns = ['event_name','user_count']
funnel_event_A.head()
| event_name | user_count | |
|---|---|---|
| 0 | login | 2604 |
| 1 | product_page | 1685 |
| 2 | purchase | 833 |
| 3 | product_cart | 782 |
funnel_event_B = (ab_test
.query('group == "B"')
.groupby('event_name')
.agg({'user_id':'nunique'})
.reset_index()
.sort_values(by='user_id', ascending=False)
.reset_index(drop=True))
funnel_event_B.columns = ['event_name','user_count']
funnel_event_B
| event_name | user_count | |
|---|---|---|
| 0 | login | 876 |
| 1 | product_page | 493 |
| 2 | purchase | 249 |
| 3 | product_cart | 244 |
#доля каждого события от общего числа
round(ab_test.groupby(by='event_name')['user_id'].count().sort_values(ascending=False)/len(ab_test)*100,2)
event_name login 45.17 product_page 27.95 purchase 13.72 product_cart 13.17 Name: user_id, dtype: float64
#цвета диаграммы
colors = sn.color_palette('pastel')[ 0:5 ]
#круговая диаграмма
fig, ax = plt.subplots(1, 2, figsize=(10,5))
ax[0].set_title("Распределение пользователей по событиям для группы А", size=9)
ax[0].pie(funnel_event_A['user_count'],labels = funnel_event_A['event_name'],colors = colors, autopct='%.0f%%')
ax[1].set_title("Распределение пользователей по событиям для группы B", size=9)
ax[1].pie(funnel_event_B['user_count'],labels = funnel_event_B['event_name'],colors = colors, autopct='%.0f%%')
plt.show()
Примем следйющие порядок событий:
регистрация(вход на сайт) - login.
конверсии в просмотр карточек товаров — событие product_page,
просмотры корзины — product card,
покупки — purchase.
#добавим столбец с долей пользователей для каждого события
funnel_event_B['users_share'] = round(funnel_event_B['user_count']/ab_test.query('group=="B"')['user_id'].nunique()*100,2)
funnel_event_B
| event_name | user_count | users_share | |
|---|---|---|---|
| 0 | login | 876 | 99.89 |
| 1 | product_page | 493 | 56.21 |
| 2 | purchase | 249 | 28.39 |
| 3 | product_cart | 244 | 27.82 |
#добавим столбец с долей пользователей для каждого события
funnel_event_A['users_share'] = round(funnel_event_A['user_count']/ab_test.query('group=="A"')['user_id'].nunique()*100,2)
funnel_event_A
| event_name | user_count | users_share | |
|---|---|---|---|
| 0 | login | 2604 | 100.00 |
| 1 | product_page | 1685 | 64.71 |
| 2 | purchase | 833 | 31.99 |
| 3 | product_cart | 782 | 30.03 |
#сделаем правильный порядок событий
funnel_event_A = funnel_event_A.reindex([0,1,3,2])
funnel_event_B = funnel_event_B.reindex([0,1,3,2])
funnel_event_A
| event_name | user_count | users_share | |
|---|---|---|---|
| 0 | login | 2604 | 100.00 |
| 1 | product_page | 1685 | 64.71 |
| 3 | product_cart | 782 | 30.03 |
| 2 | purchase | 833 | 31.99 |
#определим долю пользователей, которые переходят с одного этапа на следующий
funnel_event_A['share_previous'] = round(funnel_event_A['user_count'] / funnel_event_A['user_count'].shift(1)*100,2)
funnel_event_A.loc[0,'share_previous'] = 100
funnel_event_A
| event_name | user_count | users_share | share_previous | |
|---|---|---|---|---|
| 0 | login | 2604 | 100.00 | 100.00 |
| 1 | product_page | 1685 | 64.71 | 64.71 |
| 3 | product_cart | 782 | 30.03 | 46.41 |
| 2 | purchase | 833 | 31.99 | 106.52 |
#определим долю пользователей, которые переходят с одного этапа на следующий
funnel_event_B['share_previous'] = round(funnel_event_B['user_count'] / funnel_event_B['user_count'].shift(1)*100,2)
funnel_event_B.loc[0,'share_previous'] = 100
funnel_event_B
| event_name | user_count | users_share | share_previous | |
|---|---|---|---|---|
| 0 | login | 876 | 99.89 | 100.00 |
| 1 | product_page | 493 | 56.21 | 56.28 |
| 3 | product_cart | 244 | 27.82 | 49.49 |
| 2 | purchase | 249 | 28.39 | 102.05 |
#строим воронку для группы A
fig = go.Figure(go.Funnel(
y = funnel_event_A['event_name'],
x = funnel_event_A['user_count'],
textinfo = "value+percent initial+percent previous"))
fig.update_layout(title_text='Воронка событий для группы A')
fig.show()
#строим воронку для группы В
fig = go.Figure(go.Funnel(
y = funnel_event_B['event_name'],
x = funnel_event_B['user_count'],
textinfo = "value+percent initial+percent previous"))
fig.update_layout(title_text='Воронка событий для группы В')
fig.show()
Сравнив воронки событий для групп А и В мы видим, что для обоих групп показатель конверсии в покупку низкий около 30% (32% для группы А, 28% для группы В), и на каждом из шагов у нас "отсеивается" примерно половина пользователей. Увеличения конверсии в покупку на 10% не произошло, а она наоборот упала на 12%.
Изменение конверсии в воронке в выборках на разных этапах:
для группы А: 65% перешли на карточку товара -> 30% перешли в продуктовую корзину -> 32% совершили покупку.
для группы B: 56% перешли на карточку товара -> 28% перешли в продуктовую корзину -> 28% совершили покупку.
Еще раз отметим наше наблюдение, что количество пользователей, совершивших покупку больше пользователей, перешедших в корзину. Вероятно, пользователи могут покупать товар напрямую с карточки (страницы) товара.
#распределение событий по дням
sn.set_style("whitegrid")
plt.figure(figsize=(10,5))
sn.histplot(data = ab_test, x='event_dt',bins=24*24, hue='group', palette='muted')
plt.grid()
plt.title('Гистограмма распределения событий по дням')
plt.xlabel('Дата')
plt.ylabel('Количество событий')
plt.axvspan('2020-12-25', '2021-01-03', color='green', alpha=.1)
#plt.xlim('2020-12-07 00:00:00','2021-01-03')
plt.tight_layout()
plt.show()
Основные пики актиности пользователей заметны 14 декбря и 21 декабря - дата окончания набора новых пользователей.
#среднее количество событий на пользователя
print('Среднее количество событий на пользователя для группы A', round(ab_test.query('group=="A"').groupby('user_id', as_index=False).agg({'event_name':'count'}).mean(),2))
#средее количество уникальных событий на пользователя
print('Среднее количество событий на пользователя для группы A', round(ab_test.query('group=="A"').groupby('user_id', as_index=False).agg({'event_name':'nunique'}).mean(),2))
Среднее количество событий на пользователя для группы A event_name 6.85 dtype: float64 Среднее количество событий на пользователя для группы A event_name 2.27 dtype: float64
#среднее количество событий на пользователя
print('Среднее количество событий на пользователя для группы B', round(ab_test.query('group=="B"').groupby('user_id', as_index=False).agg({'event_name':'count'}).mean(),2))
#средее количество уникальных событий на пользователя
print('Среднее количество событий на пользователя для группы B', round(ab_test.query('group=="B"').groupby('user_id', as_index=False).agg({'event_name':'nunique'}).mean(),2))
Среднее количество событий на пользователя для группы B event_name 5.46 dtype: float64 Среднее количество событий на пользователя для группы B event_name 2.12 dtype: float64
#количество событий на пользователя - данные для гистограммы
event_by_user = ab_test.groupby(['group','user_id'], as_index=False).agg(count_event=('event_name','count')).reset_index()
plt.figure(figsize=(15, 5))
plt.title('Распределение количества событий на пользователя по группам теста')
sn.histplot(data = event_by_user, x='count_event', bins=50, hue='group', kde=True)
plt.xlabel('Количество событий')
plt.ylabel('Количество пользователей')
#plt.legend()
plt.show()
Заметно, что в среднем на пользователя группы А приходится больше событий, чем на пользователя группы В.
Среднее количество событий на пользователя для группы A - 6,85.
Среднее количество событий на пользователя для группы A - 2,27.
Среднее количество событий на пользователя для группы B - 5,46.
Среднее количество событий на пользователя для группы B - 2,12.
Особенности, выявленные в ходе анализа:
В данных присутсвует второй тест interface_eu_test, у которого регион проведения также Европа, но даты проведения неизвестны. Из ТЗ мы знаем, что это конкурирубщий тест, и он может оказывать влияние на наш. Не стали исключать пользователей, попавших в конкурирующий тест, так как они распределены равномерно между группами нашего теста.
В пользотелей теста попали не только жители Европы, но и стран СНГ, Азии и Северной Америки. Это может быть сбой при проведении теста, либо пользователи использовали VPN.
Согласно ТЗ тест проводится с 7 декбря 2020 по 4 января 2021, но в данных отсутствует период с 30-12 по 04-01. С чем это связано? Ошибка при выгрузке данных или технический сбой при проведении теста?
Тестирование recommender_system_test пересеклось с "Рождественским&Новогодним промо", но судя по распределению событий по дням особого влияние не оказало.
Количество пользователей соответсвует ожидаемому по а/б калькулятору, но пользователи распределены между группами неравномерно. Группа А - 2604, группа В - 877. Равный размер групп наиболее оптимален при проведении теста.
Продуктовая воронка выглядит следующим образом: вход -> карточка товара -> переход в корзину -> покупка.
При постороении воронок по группам А и B было замечено, что доля пользователей, которые переходят в корзину, меньше доли пользователей, совершивших покупки. Выдвинули предположение о том, что пользователи могут совершать покупки напрямую из карточки товара, минуя корзину.
Для сравнения долей перехода с одного события на другое в двух группах будем использовать z-тест. Проведем сравнение пропорций для основных метрик - product_page, product_cart и purchase.
Сформулируем гипотезы.
Нулевая гипотеза: различий в долях конкретного события между группами нет.
Альтернативная: различия в долях между группами есть.
В паре будет 3 гипотезы (конверсии будут сраниваться для 3 событий в продуктовой воронке). Таким образом, суммарно будет провередено 1*3=3 теста.
При проведении множественного теста, чтобы снизить вероятность ошибки первого рода (нулевая гипотеза неверно отвергнута), необходимо применить поправку к уровню значимости. Будем использовать поправку Бонферрони - уровни значимости в каждом из m сравнений в m раз меньше, чем уровень значимости, требуемый при единственном сравнении.
Уровень значимости примем - 5%.
#количество пользователей в каждой группе
users_by_group = ab_test.pivot_table(index='group',values = 'user_id', aggfunc = 'nunique').reset_index()
users_by_group
| group | user_id | |
|---|---|---|
| 0 | A | 2604 |
| 1 | B | 877 |
#группируем данные по группам и событиям для z-теста, сортируем по популярности событий
for_z_test = ab_test.query('event_name != "login"').pivot_table(index = 'event_name', columns = 'group', values = 'user_id', aggfunc = 'nunique',margins=True)
for_z_test = for_z_test.sort_values(by="A", ascending=False)
for_z_test
| group | A | B | All |
|---|---|---|---|
| event_name | |||
| All | 2120 | 666 | 2786 |
| product_page | 1685 | 493 | 2178 |
| purchase | 833 | 249 | 1082 |
| product_cart | 782 | 244 | 1026 |
def z_test(event_1, event_2, total_1, total_2, bonferroni, alpha=0.05):
'''
функция для проведения z-теста, где event_1 - количество пользователей, совершивших действие в 1 группе
event_2 - количество пользователей, совершивших действие в 2 группе
total_1 - суммарное количество пользователей в 1-й группе
total_2 - суммарное количество пользователей в 2-й группе
bonferroni - количество сравниваемых пар
alpha - уровень статистической значимости
'''
#статистический уровень значимости с учетом поправки
bonferroni_alpha = alpha / bonferroni
#доля успехов в 1 группе
share_1 = event_1 / total_1
#доля успехов в 2 группе
share_2 = event_2 / total_2
#доля успехов в комбинированном датасете
share_combined = (event_1 + event_2) / (total_1 + total_2)
#разница в долях между группами
diff = share_1 - share_2
#считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = diff / (share_combined * (1 - share_combined) * (1/total_1 + 1/total_2)) ** 0.5
#задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
dist = stats.norm(0, 1)
#считаем по статистичке p-value
p_value = 2 * (1 - dist.cdf(abs(z_value)))
print('p-значение: ', p_value)
#сравниваем p_value с уровнем значимости и делаем вывод по тесту
if p_value < bonferroni_alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
print('')
#запустим функцию для сравнения долей групп по всем событиям
for event in for_z_test.index:
if event!='All':
print('Результаты теста для события',event )
z_test(for_z_test.loc[event, 'A'], for_z_test.loc[event, 'B'], for_z_test.loc['All','A'], for_z_test.loc['All','B'], 3)
print('')
Результаты теста для события product_page p-значение: 0.0029370596204436605 Отвергаем нулевую гипотезу: между долями есть значимая разница Результаты теста для события purchase p-значение: 0.37888740748690086 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Результаты теста для события product_cart p-значение: 0.9070540547116017 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Нет оснований отвергать нулевую гипотезу для событий "purchase" и "product_cart", статистически значимой разницы не выявлено, а значит улучшение рекомендательной системы не повлияло на поведение пользователей.
Для события "product_page" статистически значимые различия между долями групп А и В обнаружены.
Выводы
В результате выявленных особенностей на этапе исследовательского анализа, можно сделать вывод о том, что тест проведен с нарушениями:
Тест проведен в период повышенного спроса обусловленного сезонностью - увеличение количества покупок в связи с Новым годом и Рождеством.В предновогодние/новогодние праздники покупательская активность сильно меняется. Все гипотезы, которые мы сейчас подтвердили, могут показать совершенно другие результаты, например, весной, когда покупательское поведение будет другое.
Тестирование recommender_system_test пересеклось с "Рождественским&Новогодним промо", чего необходимо избегать при проведении а/б теста.
Выяснили, что в регионе проходил еще один конкурирующий тест (даты его проведения не знаем). После дополнительной проверки мы пришли к выводу, что пользователи, попавшие в оба теста, равномерно распределены между группами нашего теста. Поэтому не стали исключать их из анализа.
В датафрейме нет данных о событиях с 31 декабря по 04 января 2021 - дата окончания теста согласно ТЗ. Непонятно, почему данные отсутсвуют.
Количество уникальных пользователей в группах А и В сильно различается (2604 и 877 соответственно). Причем изначально было 6701 пользователь (A - 3634, В - 2717), но как оказалось больше половины пользователей не совершали действий.
Таким образом, все особенности могли оказать влияние на результаты теста.
Проанализировав события были принята следующая последовательность вороки: Регистрация -> Кароточка товара -> Переход в коризину -> Покупка. При построении продуктовой воронки оказалось, что пользователей, совершивших покупки больше, чем пользователей перешедших в корзину. При составлении ТЗ необходимо было указать, могут ли пользователи напрямую совершать покупку с карточки товара, чтобы исключить двойственную интерпретацию наблюдений. А вдруг это сбой в записи данных?
Результаты проверки гипотезы о равенстве долей уникальных пользователей следующие:
для события "product_page" статистически значимые различия между долями групп А и В обнаружены ( по продуктовой воронке конверсия относительно первого этапа для группы А - 65%, для группы В - 56%);
статистически значимой разницы не выявлено в долях уникальных пользователей между группами А и В для событий "purchase" и "product_cart".
Таким образом, по имеющимся результатам теста улучшеная рекомендательная система не повлияла на поведение пользователей (и, следовательно, не улучшила метрики).
В виду отмеченных особенностей рекомендуется признать тест некорректым, доработать механизм формирования тестовых групп и запустить тест заново, избегая наложения с маркетинговыми акциями и явными сезонными всплесками.